/*
 * Copyright (C) 2019 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { Headers } from './Headers';
import { indexOfNonWhitespace, toNonNegativeInt } from './utils/StringUtil';
import { kotlinRequire } from './utils/Utils';
import { TimeUnit } from './utils/TimeUnit';


/**
 * A Cache-Control header with cache directives from a server or client. These directives set policy
 * on what responses can be stored, and which requests can be satisfied by those stored responses.
 *
 * See [RFC 7234, 5.2](https://tools.ietf.org/html/rfc7234#section-5.2).
 */
export class CacheControl{
  // Integer.MAX_VALUE in Java
  private static INT_MAX_VALUE = 2147483647

  /**
   * Clamp a value to Integer.MAX_VALUE
   */
  private static clampToInt(value: number): number {
    return value > CacheControl.INT_MAX_VALUE ? CacheControl.INT_MAX_VALUE : Math.floor(value)
  }

  /**
   * In a response, this field's name "no-cache" is misleading. It doesn't prevent us from caching
   * the response; it only means we have to validate the response with the origin server before
   * returning it. We can do this with a conditional GET.
   *
   * In a request, it means do not use a cache to satisfy the request.
   */
  // @get:JvmName("noCache")
  _noCache: boolean
  get noCache(){
    return this._noCache;
  }

  /** If true, this response should not be cached. */
  // @get:JvmName("noStore")
  _noStore: boolean
  get noStore(){
    return this._noStore;
  }

  /** The duration past the response's served date that it can be served without validation. */
  // @get:JvmName("maxAgeSeconds") val
  _maxAgeSeconds: number
  get maxAgeSeconds(){
    return this._maxAgeSeconds;
  }

  /**
   * The "s-maxage" directive is the max age for shared caches. Not to be confused with "max-age"
   * for non-shared caches, As in Firefox and Chrome, this directive is not honored by this cache.
   */
  // @get:JvmName("sMaxAgeSeconds") val
  _sMaxAgeSeconds: number
  get sMaxAgeSeconds(){
    return this._sMaxAgeSeconds;
  }

  isPrivate: boolean
  isPublic: boolean

  // @get:JvmName("mustRevalidate") val
  _mustRevalidate: boolean
  get mustRevalidate(){
    return this._mustRevalidate;
  }

  // @get:JvmName("maxStaleSeconds") val
  _maxStaleSeconds: number
  get maxStaleSeconds(){
    return this._maxStaleSeconds;
  }

  // @get:JvmName("minFreshSeconds") val
  _minFreshSeconds: number
  get minFreshSeconds(){
    return this._minFreshSeconds;
  }

  /**
   * This field's name "only-if-cached" is misleading. It actually means "do not use the network".
   * It is set by a client who only wants to make a request if it can be fully satisfied by the
   * cache. Cached responses that would require validation (ie. conditional gets) are not permitted
   * if this header is set.
   */
  // @get:JvmName("onlyIfCached")
  _onlyIfCached: boolean
  get onlyIfCached(){
    return this._onlyIfCached;
  }

  // @get:JvmName("noTransform")
  _noTransform: boolean
  get noTransform(){
    return this._noTransform;
  }

  // @get:JvmName("immutable")
  _immutable: boolean
  get immutable(){
    return this._immutable;
  }

  private headerValue: string | null


  constructor(noCache: boolean, noStore: boolean, maxAgeSeconds: number, sMaxAgeSeconds: number, isPrivate: boolean, isPublic: boolean, mustRevalidate: boolean, maxStaleSeconds: number, minFreshSeconds: number, onlyIfCached: boolean, noTransform: boolean, immutable: boolean, headerValue: string | null) {
    this._noCache = noCache
    this._noStore = noStore
    this._maxAgeSeconds = maxAgeSeconds
    this._sMaxAgeSeconds = sMaxAgeSeconds
    this.isPrivate = isPrivate
    this.isPublic = isPublic
    this._mustRevalidate = mustRevalidate
    this._maxStaleSeconds = maxStaleSeconds
    this._minFreshSeconds = minFreshSeconds
    this._onlyIfCached = onlyIfCached
    this._noTransform = noTransform
    this._immutable = immutable
    this.headerValue = headerValue
  }

  toString(): string {
    let result = this.headerValue
    if (result === null || result === undefined) {
      result = ''
      if (this._noCache){
        result += "no-cache, "
      }
      if (this._noStore){
        result += "no-store, "
      }
      if (this._maxAgeSeconds !== -1){
        result += "max-age=".concat(this._maxAgeSeconds.toString()).concat(", ")
      }
      if (this._sMaxAgeSeconds !== -1){
        result += "s-maxage=".concat(this._sMaxAgeSeconds.toString()).concat(", ")
      }
      if (this.isPrivate){
        result += "private, "
      }
      if (this.isPublic){
        result += "public, "
      }
      if (this._mustRevalidate){
        result += "must-revalidate, "
      }
      if (this._maxStaleSeconds != -1){
        result += "max-stale=".concat(this._maxStaleSeconds.toString()).concat(", ")
      }
      if (this._minFreshSeconds != -1){
        result += "min-fresh=".concat(this._minFreshSeconds.toString()).concat(", ")
      }
      if (this._onlyIfCached){
        result += "only-if-cached, "
      }
      if (this._noTransform){
        result += "no-transform, "
      }
      if (this._immutable){
        result += "immutable, "
      }
      if (result === '') return ''
      result = result.substring(0, result.length - 2)
      this.headerValue = result
    }
    return result
  }

  /** Builds a `Cache-Control` request header. */
  static Builder = class Builder {
    #noCache: boolean = false
    #noStore: boolean = false
    #maxAgeSeconds = -1
    #maxStaleSeconds = -1
    #minFreshSeconds = -1
    #onlyIfCached: boolean = false
    #noTransform: boolean = false
    #immutable: boolean = false

    /** Don't accept an unvalidated cached response. */
    noCache(){
      this.#noCache = true
      return this
    }

    /** Don't store the server's response in any cache. */
    noStore(){
      this.#noStore = true
      return this
    }

    /**
     * Sets the maximum age of a cached response. If the cache response's age exceeds [maxAge], it
     * will not be used and a network request will be made.
     *
     * @param maxAge a non-negative integer. This is stored and transmitted with [TimeUnit.SECONDS]
     *     precision; finer precision will be lost.
     */
    maxAge(maxAge: number, timeUnit: TimeUnit){
      kotlinRequire(maxAge >= 0,"maxAge < 0: $maxAge")
      let maxAgeSeconds = timeUnit.toSeconds(maxAge)
      this.#maxAgeSeconds = CacheControl.clampToInt(maxAgeSeconds)
      return this
    }

    /**
     * Accept cached responses that have exceeded their freshness lifetime by up to `maxStale`. If
     * unspecified, stale cache responses will not be used.
     *
     * @param maxStale a non-negative integer. This is stored and transmitted with
     *     [TimeUnit.SECONDS] precision; finer precision will be lost.
     */
    maxStale(maxStale: number, timeUnit: TimeUnit){
      kotlinRequire(maxStale >= 0,"maxStale < 0: $maxStale")
      let maxStaleSeconds = timeUnit.toSeconds(maxStale)
      this.#maxStaleSeconds = CacheControl.clampToInt(maxStaleSeconds)
      return this
    }

    /**
     * Sets the minimum number of seconds that a response will continue to be fresh for. If the
     * response will be stale when [minFresh] have elapsed, the cached response will not be used and
     * a network request will be made.
     *
     * @param minFresh a non-negative integer. This is stored and transmitted with
     *     [TimeUnit.SECONDS] precision; finer precision will be lost.
     */
    minFresh(minFresh: number, timeUnit: TimeUnit){
      kotlinRequire(minFresh >= 0,"minFresh < 0: $minFresh" )
      let minFreshSeconds = timeUnit.toSeconds(minFresh)
      this.#minFreshSeconds = CacheControl.clampToInt(minFreshSeconds)
      return this
    }

    /**
     * Only accept the response if it is in the cache. If the response isn't cached, a `504
     * Unsatisfiable Request` response will be returned.
     */
    onlyIfCached(){
      this.#onlyIfCached = true
      return this
    }

    /** Don't accept a transformed response. */
    noTransform(){
      this.#noTransform = true
      return this
    }

    immutable(){
      this.#immutable = true
      return this
    }

    build(): CacheControl {
      return new CacheControl(this.#noCache, this.#noStore, this.#maxAgeSeconds, -1, false, false, false, this.#maxStaleSeconds,
        this.#minFreshSeconds, this.#onlyIfCached, this.#noTransform, this.#immutable, null)
    }
  }

  /**
   * Cache control request directives that require network validation of responses. Note that such
   * requests may be assisted by the cache via conditional GET requests.
   */
  // @JvmField
  static FORCE_NETWORK = new CacheControl.Builder()
    .noCache()
    .build()

  /**
   * Cache control request directives that uses the cache only, even if the cached response is
   * stale. If the response isn't available in the cache or requires server validation, the call
   * will fail with a `504 Unsatisfiable Request`.
   */
  // @JvmField
  static FORCE_CACHE = new CacheControl.Builder()
    .onlyIfCached()
    .maxStale(CacheControl.INT_MAX_VALUE, TimeUnit.SECONDS)
    .build()

    /**
     * Returns the cache directives of [headers]. This honors both Cache-Control and Pragma headers
     * if they are present.
     */
    // @JvmStatic
    static parse(headers: Headers): CacheControl {
      let noCache = false
      let noStore = false
      let maxAgeSeconds = -1
      let sMaxAgeSeconds = -1
      let isPrivate = false
      let isPublic = false
      let mustRevalidate = false
      let maxStaleSeconds = -1
      let minFreshSeconds = -1
      let onlyIfCached = false
      let noTransform = false
      let immutable = false

      let canUseHeaderValue = true
      let headerValue: string | null = null
      loop:
        for (let i = 0; i < headers.size; i++){
          let name = headers.name(i)
          let value = headers.value(i)
          if (name.toLowerCase() === "Cache-Control".toLowerCase()){
            if (headerValue !== null && headerValue !== undefined){
              canUseHeaderValue = false
            }else{
              headerValue = value
            }
          }else if (name.toLowerCase() === "Pragma".toLowerCase()) {
            canUseHeaderValue = false
          }else {
            continue loop
          }
          let pos = 0
          while (pos < value.length) {
            let tokenStart = pos
            pos = CacheControl.indexOfElement(value, "=,;", pos)
            let directive = value.substring(tokenStart, pos).trim()
            let parameter: string | null
            if (pos == value.length || value[pos] == ',' || value[pos] == ';') {
              pos++ // Consume ',' or ';' (if necessary).
              parameter = null
            } else {
            pos++ // Consume '='.
            pos = indexOfNonWhitespace(value,pos)
            if (pos < value.length && value[pos] == '\"') {
              // Quoted string.
              pos++ // Consume '"' open quote.
              let parameterStart = pos
              pos = value.indexOf('"', pos)
              parameter = value.substring(parameterStart, pos)
              pos++ // Consume '"' close quote (if necessary).
            } else {
              // Unquoted string.
              let parameterStart = pos
              pos = CacheControl.indexOfElement(value, ",;", pos)
              parameter = value.substring(parameterStart, pos).trim()
            }
          }
          // 大小写不敏感比较
          let directiveLower = directive.toLowerCase()
          switch (directiveLower){
            case "no-cache":
              noCache = true
              break
            case "no-store":
              noStore = true
              break
            case "max-age":
              maxAgeSeconds = toNonNegativeInt(parameter,-1)
              break
            case "s-maxage":
              sMaxAgeSeconds = toNonNegativeInt(parameter,-1)
              break
            case "private":
              isPrivate = true
              break
            case "public":
              isPublic = true
              break
            case "must-revalidate":
              mustRevalidate = true
              break
            case "max-stale":
              maxStaleSeconds = toNonNegativeInt(parameter, CacheControl.INT_MAX_VALUE)
              break
            case "min-fresh":
              minFreshSeconds = toNonNegativeInt(parameter,-1)
              break
            case "only-if-cached":
              onlyIfCached = true
              break
            case "no-transform":
              noTransform = true
              break
            case "immutable":
              immutable = true
              break
            }
          }
       }
    if (!canUseHeaderValue) {
      headerValue = null
    }
    return new CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPrivate, isPublic,
      mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached, noTransform, immutable,
      headerValue)
  }

  /**
   * Returns the next index in this at or after [startIndex] that is a character from
   * [characters]. Returns the input length if none of the requested characters can be found.
   */
  private static indexOfElement(str: string, characters: string, startIndex: number = 0): number {
    for (let i = startIndex; i < str.length; i++) {
      if (characters.indexOf(str[i]) != -1) {
        return i
      }
    }
    return str.length
  }
}
